package org.acghub.mtdb.core;

import lombok.extern.slf4j.Slf4j;
import org.acghub.mtdb.common.exception.DeadLockError;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author zhouhe
 */
@Slf4j
public abstract class AbstractProcedure<P extends AbstractProcedure> implements Procedure<P> {

    protected final int retryDelay;
    protected final int retryTimes;
    protected final Object executorKey;

    protected String name = getClass().getSimpleName();

    protected List<ProcedureListener<P>> listeners;

    private volatile Throwable cause;

    private volatile boolean success;

    public AbstractProcedure(int retryDelay, int retryTimes, Object executorKey, String name) {
        this.retryDelay = retryDelay;
        this.retryTimes = retryTimes;
        this.executorKey = executorKey;
        this.name = name;
    }

    protected AbstractProcedure(int retryDelay, int retryTimes, Object executorKey) {
        this.retryDelay = retryDelay;
        this.retryTimes = retryTimes;
        this.executorKey = executorKey;
    }

    protected AbstractProcedure(Object executorKey, String name) {
        this(500, -1, executorKey, name);
    }

    protected AbstractProcedure(Object executorKey) {
        this(500, -1, executorKey);
    }

    protected AbstractProcedure() {
        this(500, -1, null);
    }

    @Override
    public String name() {
        return name;
    }

    @Override
    public int retryDelay() {
        return retryDelay;
    }

    @Override
    public int retryTimes() {
        return retryTimes;
    }

    @Override
    public Object executorKey() {
        return executorKey;
    }

    @Override
    public ProcedureExecutor executor() {
        return Procedures.INSTANCE.selectProcedureExecutor(this.executorKey());
    }

    @Override
    public P addListener(ProcedureListener<P> listener) {
        addListener0(listener);
        return self();
    }

    @Override
    public P addListeners(Collection<ProcedureListener<P>> listeners) {
        if (listeners != null && !listeners.isEmpty()) {
            listeners.forEach(this::addListener0);
        }
        return self();
    }

    @Override
    public Collection<ProcedureListener<P>> getListeners() {
        return listeners;
    }

    @Override
    public Result call() {
        if (Transaction.current() == null) {
            try {
                Transaction.create();
                for (int retry = 0; ; ) {
                    if (perform()) {
                        break;
                    }
                    retry++;
                    if (retry > this.times()) {
                        break;
                    }
                    int delay = delay();
                    if (delay > 0) {
                        try {
                            TimeUnit.MILLISECONDS.sleep(delay);
                        } catch (InterruptedException ignored) {
                            log.warn("procedure {} sleep interrupted", name());
                        }
                    }
                }
            } finally {
                Transaction.destroy();
            }
        } else {
            call0();
        }
        return getResult();
    }

    @Override
    public void execute() {
        executor().execute(this);
    }

    @Override
    public ProcedureFuture<P> submit() {
        return (ProcedureFuture<P>) executor().submit(this);
    }

    boolean call0() {
        int savepoint = Transaction.savepoint();
        try {
            if (this.process()) {
                this.setCause(null);
                this.setSuccess(true);
                return true;
            }
        } catch (Exception e) {
            this.setCause(e);
            log.error("procedure {} execute error", getClass().getSimpleName(), e);
        }
        Transaction.rollback(savepoint);
        setSuccess(false);
        return false;
    }

    Result getResult() {
        return new Result() {
            @Override
            public boolean isSuccess() {
                return success;
            }

            @Override
            public Throwable getCause() {
                return cause;
            }
        };
    }

    private boolean perform() {
        try {
            Transaction.current().perform(this);
        } catch (DeadLockError e) {
            return false;
        } catch (Throwable ignored) {
        }
        return true;
    }

    private void addListener0(ProcedureListener<P> listener) {
        if (listener == null) {
            return;
        }
        if (listeners == null) {
            listeners = new ArrayList<>();
        }
        listeners.add(listener);
    }

    private P self() {
        return (P) this;
    }

    Throwable getCause() {
        return cause;
    }

    void setCause(Throwable cause) {
        this.cause = cause;
    }

    boolean isSuccess() {
        return success;
    }

    void setSuccess(boolean success) {
        this.success = success;
    }

    /**
     * 重试延迟时间
     *
     * @return
     */
    int delay() {
        return Math.min(Math.max(0, this.retryDelay()), MAX_RETRY_DELAY_MS);
    }

    /**
     * 重试次数
     *
     * @return
     */
    int times() {
        return Math.min(Math.max(0, this.retryTimes()), MAX_RETRY_TIMES);
    }
}
